package ade.prime.service;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import ade.prime.activity.AdeWebNotification;
import ade.prime.activity.R;
import ade.prime.activity.adapter.ResourceFetcher;
import ade.prime.analyzer.ContainerData;
import ade.prime.analyzer.FeedAnalyzer;
import ade.prime.analyzer.FeedConnection;
import ade.prime.analyzer.FeedLesson;
import ade.prime.database.Database;
import ade.prime.exception.AAAException;
import ade.prime.exception.NoConnectionException;
import ade.prime.model.AdeWebOptions;
import ade.prime.model.GuidScheduleUpdater;
import ade.prime.model.IntentConstants;
import ade.prime.model.Lesson;
import ade.prime.model.LessonType;
import ade.prime.model.Lessons;
import ade.prime.model.Project;
import ade.prime.model.Resource;
import ade.prime.model.Schedule;
import ade.prime.model.ScheduleState;
import ade.prime.model.ScheduleUpdater;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Pair;

/**
 * @author Schnell Michaël
 * @version 1.0
 */
public class AdeWebService extends Service implements
		OnSharedPreferenceChangeListener, IntentConstants {

	/**
	 * Used for logging purposes.
	 */
	protected static final String TAG = AdeWebService.class.getSimpleName();

	// private static final String AUTO_UPDATE_TIMER_NAME =
	//			"AutoUpdateTimer" ; //$NON-NLS-1$
	//
	// private static final long INITIAL_TIMER_DELAY = 1000L ;
	//
	// private static final long N_MILLIS_PER_HOURS = 60L * 60L * 1000L ;

	private static final long INITIAL_UPDATE_DELAY = 1000L;

	private static final int DEPRECATED_LIMIT_IN_DAYS = 3;

	/**
	 * Object responsible for periodically calling the update of the schedules.
	 */
	protected Timer timer;

	/**
	 * Callback doing the actual schedule update.
	 */
	protected TimerTask autoUpdateTask;

	/**
	 * List of the listeners alerted when a schedule update occurred.
	 */
	protected List<AdeWebServiceListener> listeners;

	/**
	 * API end point for this service.
	 */
	protected ApiEndPoint apiEndpoint;

	/**
	 * Queue containing the schedules waiting for an update.
	 */
	protected List<Pair<ScheduleState, Schedule>> updateQueue;

	/**
	 * Runnable used in a separate thread for updating the schedules in the
	 * update queue.
	 */
	protected ScheduledExecutorService scheduler;

	/**
	 * {@inheritDoc}
	 */
	@Override
	public IBinder onBind(Intent intent) {
		if (AdeWebService.class.getName().equals(intent.getAction())) {
			//			Log.d(TAG, "Bound by intent " + intent) ; //$NON-NLS-1$
			return this.apiEndpoint;
		}// if
		return null;
	}// onBind(Intent)

	@Override
	public void onCreate() {
		super.onCreate();
		//		Log.i(TAG, "Service creating"); //$NON-NLS-1$
		this.listeners = new ArrayList<AdeWebServiceListener>();
		this.updateQueue = new ArrayList<Pair<ScheduleState, Schedule>>();
		this.apiEndpoint = new ApiEndPoint();
		this.scheduler = Executors.newScheduledThreadPool(1);

		if (AdeWebOptions.autoUpdateIsOn(this)) {
			this.startAutoUpdates();
		}// if
		this.startScheduleStateManager();

		// Add listener for preference changes
		AdeWebOptions.registerChangeListener(this, this);
	}// onCreate()

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent != null) {
			String action = intent.getAction();
			//			Log.d(TAG, String.format("[onStartCommand] action: '%s'", //$NON-NLS-1$
			// action)) ;
			if (action.equals(SCHEDULE_STATE_UPDATE_ACTION)) {
				this.updateDeprecatedScheduleStates();
			} else if (action.equals(SCHEDULE_AUTO_UPDATE_ACTION)) {
				this.launchAutoUpdate();
			} else if (action.equals(REPLICATE_PREFERENCES_ACTION)) {
				this.replicatePreferences(intent.getExtras());
			}// if
		}// if
		return super.onStartCommand(intent, flags, startId);
	}// onStartCommand(Intent, int, int)

	@Override
	public void onDestroy() {
		super.onDestroy();
		//		Log.i(TAG, "Service destroying") ; //$NON-NLS-1$

		// Remove listener for preference changes
		AdeWebOptions.unregisterChangeListener(this, this);

		if (this.timer != null) {
			this.cancelAutoUpdates();
		}// if
	}// onDestroy()

	protected void startAutoUpdates() {
		// this.timer = new Timer(AUTO_UPDATE_TIMER_NAME) ;
		// this.autoUpdateTask = new AutoUpdateTask() ;
		// int nHours = AdeWebOptions.getnHoursBetweenUpdates(this) ;
		// long updateDelay = nHours * N_MILLIS_PER_HOURS ;
		// this.timer.schedule(this.autoUpdateTask, INITIAL_TIMER_DELAY,
		// updateDelay) ;
		long nHours = AdeWebOptions.getnHoursBetweenUpdates(this);
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE);
		Intent intent = new Intent(SCHEDULE_AUTO_UPDATE_ACTION);
		PendingIntent autoUpdateBroadcast = PendingIntent.getBroadcast(this, 0,
				intent, 0);
		am.setRepeating(AlarmManager.RTC, System.currentTimeMillis() + 10L,
				nHours * 60L * 60L * 1000L, autoUpdateBroadcast);
	}// startAutoUpdates()

	protected void cancelAutoUpdates() {
		// this.timer.cancel() ;
		// this.timer = null ;
		// this.autoUpdateTask = null ;
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE);
		Intent intent = new Intent(SCHEDULE_AUTO_UPDATE_ACTION);
		PendingIntent autoUpdateBroadcast = PendingIntent.getBroadcast(this, 0,
				intent, 0);
		am.cancel(autoUpdateBroadcast);
	}// cancelAutoUpdates()

	private void startScheduleStateManager() {
		// Start the "schedule status manager"
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE);
		Intent intent = new Intent(SCHEDULE_STATE_UPDATE_ACTION);
		PendingIntent scheduleStateBroadCast = PendingIntent.getBroadcast(this,
				0, intent, 0);
		am.setRepeating(AlarmManager.RTC, System.currentTimeMillis() + 100000L,
				24L * 60L * 60L * 1000L, scheduleStateBroadCast);
	}// startScheduleStateManager()

	// /**
	// * Callback for the timer doing the actual schedule update.
	// *
	// * @author Schnell Michaël
	// * @version 1.0
	// */
	// private class AutoUpdateTask extends TimerTask {
	//
	// /**
	// * Sole constructor.
	// */
	// protected AutoUpdateTask() {
	// super() ;
	// }// SchedulUpdate()
	//
	// /**
	// * Called by the timer, updates the schedules.
	// */
	// @Override
	// public void run() {
	// AdeWebService service = AdeWebService.this ;
	// synchronized (service.updateQueue) {
	// try {
	// for(Schedule schedule : service.apiEndpoint.getAllSchedules()) {
	// service.apiEndpoint.updateSchedule(schedule) ;
	// }// for
	// } catch (RemoteException re) {
	// Log.d(AdeWebService.TAG,
	//							"Failed to auto-update all schedules.", re) ; //$NON-NLS-1$
	// }// try
	// }// synchronized
	// }// run
	//
	// }// class AutoUpdateTask

	private class ApiEndPoint extends AdeWebServiceApi.Stub {

		protected int nextListenerId;

		private AdeWebService service;

		protected final Object databaseWriteLock;

		/**
		 * 
		 */
		public ApiEndPoint() {
			super();
			this.service = AdeWebService.this;
			this.databaseWriteLock = new Object();
			this.nextListenerId = 0;
		}// ApiEndPoint()

		@Override
		public int addListener(AdeWebServiceListener listener)
				throws RemoteException {
			synchronized (this.service.listeners) {
				this.service.listeners.add(listener);
			}// synchronized
			return this.nextListenerId++;
		}// addListener(AdeWebServiceListener)

		@Override
		public void removeListener(int listenerId) throws RemoteException {
			synchronized (this.service.listeners) {
				Iterator<AdeWebServiceListener> iter = this.service.listeners
						.iterator();
				boolean removed = false;
				while (iter.hasNext() && !removed) {
					AdeWebServiceListener next = iter.next();
					if (next.getId() == listenerId) {
						iter.remove();
						removed = true;
					}// if
				}// while
			}// synchronized
		}// removeListener(AdeWebServiceListener)

		@Override
		public Schedule createSchedule(String name, Resource[] resources,
				int period, Project project) throws RemoteException {
			//			Log.d(TAG, "Service - Creating schedule, values:") ; //$NON-NLS-1$
			//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
			//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
			//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
			//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(name, resources, period, project);
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				try {
					schedule.validate(db);
					db.insertSchedule(schedule);
				} catch (AAAException ae) {
					Log.d(TAG, "-- Failed validate.", ae); //$NON-NLS-1$
					schedule = null;
				}// try
			}// synchronized
				//			Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
			if (schedule != null) {
				synchronized (this.service.listeners) {
					for (AdeWebServiceListener listener : this.service.listeners) {
						listener.handleNewSchedule(schedule);
					}// for
				}// synchronized
			}// if
			return schedule;
		}// createSchedule(String, Resource[],int, Project)

		@Override
		public Schedule editSchedule(Schedule oldSchedule, String name,
				Resource[] resources, int period, Project project)
				throws RemoteException {
			//			Log.d(TAG, "Service - Modifying schedule, values:") ; //$NON-NLS-1$
			//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
			//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
			//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
			//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(oldSchedule);
			schedule.setName(name);
			schedule.setResources(resources);
			schedule.setPeriod(period);
			schedule.setProject(project);
			if (!oldSchedule.equals(schedule)) {
				String oldName = oldSchedule.getName();
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service);
					try {
						if (oldName.equals(name)) {
							schedule.validateOnUpdate();
						} else {
							schedule.validate(db);
						}// if
						db.updateSchedule(oldSchedule, schedule);
					} catch (AAAException ae) {
						Log.d(TAG, "-- Failed validate.", ae); //$NON-NLS-1$
						schedule = null;
					}// try
				}// synchronized
					//				Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
				if (schedule != null) {
					synchronized (this.service.listeners) {
						for (AdeWebServiceListener listener : this.service.listeners) {
							listener.handleModifiedSchedule(schedule);
						}// for
					}// synchronized
				}// if
			}// if
			return schedule;
		}// editSchedule(Schedule, String, Resource[], int, Project)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void updateScheduleState(int scheduleId, ScheduleState newState)
				throws RemoteException {
			Schedule schedule = this.getSchedule(scheduleId);
			this.updateScheduleState(schedule, newState);
		}// updateScheduleState(int, ScheduleState)

		@Override
		public Schedule updateScheduleNotify(Schedule schedule)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				db.updateSchedule(schedule.getId(), schedule);
			}// synchronized
			return schedule;
		}// editSchedule(Schedule)

		@Override
		public void removeSchedule(Schedule schedule) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				db.removeSchedule(schedule.getId());
			}// synchronized
			synchronized (this.service.listeners) {
				for (AdeWebServiceListener listener : this.service.listeners) {
					listener.handleRemovedSchedule(schedule);
				}// for
			}// synchronized
		}// removeSchedule(Schedule)

		@Override
		public void updateSchedule(Schedule schedule) throws RemoteException {
			this.updateSchedule(schedule, ScheduleState.NOT_UPDATED);
		}// updateSchedule(Schedule)

		protected void updateSchedule(Schedule schedule,
				ScheduleState failureState) {
			if (AdeWebOptions.networkIsAvailable(this.service)) {
				Schedule addedSchedule = null;
				synchronized (this.service.updateQueue) {
					if (!this.service.updateQueue.contains(schedule)) {
						addedSchedule = new Schedule(schedule);
						Pair<ScheduleState, Schedule> scheduleWithState = new Pair<ScheduleState, Schedule>(
								failureState, addedSchedule);
						this.service.updateQueue.add(scheduleWithState);
						this.service.startScheduleUpdateTask();
					}// if
				}// synchronized
				if (addedSchedule != null) {
					this.updateScheduleState(addedSchedule,
							ScheduleState.WAITING_FOR_UPDATE);
				}// if
			}// if
		}// updateSchedule(Schedule)

		protected void updateScheduleState(Schedule schedule,
				ScheduleState newState) {
			// Log.d(TAG,
			// String.format("[updateScheduleState]: %s (%s) - %s.%s",
			// schedule.getName(), Integer.valueOf(schedule.getId()),
			// newState, newUpdateState)) ;
			if (!schedule.getState().equals(newState)) {
				schedule.setState(newState);
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service);
					db.updateScheduleState(schedule.getId(), newState);
				}// synchronized
			}// if
			synchronized (this.service.listeners) {
				for (AdeWebServiceListener listener : this.service.listeners) {
					try {
						listener.handleModifiedScheduleState(schedule);
					} catch (RemoteException e) {
						// TODO Handle
						Log.d(TAG,
								"Failed to notify of schedule state change", e); //$NON-NLS-1$
					}// try
				}// for
			}// synchronized
		}// updateScheduleState(Schedule, ScheduleState)

		@Override
		public void cancelScheduleUpdate(Schedule schedule)
				throws RemoteException {
			boolean stateChange = false;
			if (!schedule.getState().equals(ScheduleState.UPDATING)) {
				synchronized (this.service.updateQueue) {
					if (this.service.updateQueue.contains(schedule)) {
						this.service.updateQueue.remove(schedule);
						stateChange = true;
					}// if
				}// synchronized
				if (stateChange) {
					this.updateScheduleState(schedule,
							ScheduleState.NOT_UPDATED);
				}// if
			}// if
		}// cancelScheduleUpdate(Schedule)

		@Override
		public List<Schedule> getAllSchedules() throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				return db.getAllSchedules();
			}// synchronized
		}// getAllSchedules()

		@Override
		public ScheduleState getScheduleState(Schedule schedule)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				return db.getSchedule(schedule.getId()).getState();
			}// synchronized
		}// getScheduleState(Schedule)

		@Override
		public Lesson getLesson(long lessonId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				return db.getLesson(lessonId);
			}// synchronized
		}// getLesson(long)

		@Override
		public LessonType getLessonType(Lesson lesson) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				return db.getLesson(lesson.getLessonId()).getType();
			}// synchronized
		}// getLessonType(Lesson)

		@Override
		public Lesson setLessonType(Lesson lesson, LessonType type)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				lesson.setType(type);
				db.updateLesson(lesson.getLessonId(), lesson);
			}// synchronized
			synchronized (this.service.listeners) {
				for (AdeWebServiceListener listener : this.service.listeners) {
					listener.handleModifiedLessonType(lesson);
				}// for
			}// synchronized
			return lesson;
		}// setLessonType(Lesson lessen, LessonType)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Schedule getSchedule(int scheduleId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				return db.getSchedule(scheduleId);
			}// synchronized
		}// getSchedule(int)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getAllScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				lessons = db.getAllScheduleLessons(scheduleId);
			}// synchronized
			return new Lessons(lessons);
		}// getAllScheduleLessons(String)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getFutureScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				lessons = db.getFutureScheduleLessons(scheduleId);
			}// synchronized
			return new Lessons(lessons);
		}// getFutureScheduleLessons(String)

		/**
		 * {@inheritDoc}
		 * 
		 * @throws RemoteException
		 */
		@Override
		public void removeLessonHistory(long lessonId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				db.removeLessonHistory(lessonId);
			}// synchronized
			synchronized (this.service.listeners) {
				for (AdeWebServiceListener listener : this.service.listeners) {
					listener.handleRemovedLessonHistory(lessonId);
				}// for
			}// synchronized
		}// removeLessonHistory(long)

		@Override
		public Project[] getAllProjects() throws RemoteException {
			Project[] projects;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service);
				projects = db.getAllProjects();
			}// synchronized
			return projects;
		}// getAllProjects()

	}// class ApiEndPoint()

	protected void startScheduleUpdateTask() {
		this.scheduler.schedule(new ScheduleUpdateRunnable(),
				INITIAL_UPDATE_DELAY, TimeUnit.MILLISECONDS);
	}// startScheduleUpdateTask()

	private class ScheduleUpdateRunnable implements Runnable {
		protected ScheduleUpdateRunnable() {
			super();
		}// ScheduleUpdateRunnable()

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void run() {
			AdeWebService.this.updateAllSchedules();
		}// run()
	}// class ScheduleUpdateRunnable

	protected void updateAllSchedules() {
		AdeWebNotification.initScheduleNotificationNumber();
		while (!this.updateQueue.isEmpty()) {

			Pair<ScheduleState, Schedule> updateValues;
			synchronized (this.updateQueue) {
				updateValues = this.updateQueue.get(0);
				this.updateQueue.remove(0);
			}// synchronized

			Database db = new Database(this);
			Schedule schedule = db.getSchedule(updateValues.second.getId());
			db.close();
			boolean notify = schedule.isNotify();
			this.updateSchedule(schedule, updateValues.first, notify);
		}// while
	}// updateAllSchedules(boolean)

	protected void updateSchedule(Schedule schedule,
			ScheduleState failureState, boolean notify) {
		Database db = new Database(this);
		try {
			this.connectToAdeApi(schedule);
			this.setProject(schedule);
			this.getLessonsFeed(schedule);
			this.analyzeLessonsFeed(schedule);
			synchronized (this.apiEndpoint.databaseWriteLock) {
				this.compareWithPreviousSchedule(schedule, db);
				this.updateScheduleInsideDatabase(schedule, db);
			}// synchronized
			ScheduleState.UPDATED.step = ScheduleUpdateProgress.STEP_6;
			this.apiEndpoint.updateScheduleState(schedule,
					ScheduleState.UPDATED);
			if (notify) {
				// Check if any lesson planned within the next two days was
				// modified
				if (this.scheduleDiff.importantChange()) {
					AdeWebNotification.makeScheduleNotification(this, schedule);
				}// if
			}// if
			this.modifyScheduleLastUpdate(schedule);
		} catch (Exception ae) {
			Log.d(TAG, String.format("Failed to update schedule %s (%d)", //$NON-NLS-1$
					schedule.getName(), Integer.valueOf(schedule.getId())), ae);
			failureState.step = schedule.getState().step;
			this.apiEndpoint.updateScheduleState(schedule, failureState);
		} finally {
			db.close();
		}// try
	}// updateSchedule(Schedule)

	protected void modifyScheduleLastUpdate(Schedule schedule) {
		String now = Database.dateFormatter.format(new Date());
		schedule.setLastUpdate(now);
		Database db = new Database(this);
		db.updateSchedule(schedule.getId(), schedule);
		db.close();
		for (AdeWebServiceListener listener : this.listeners) {
			try {
				listener.handleModifiedSchedule(schedule);
			} catch (RemoteException re) {
				Log.d(TAG,
						String.format(
								"Failed to notify of change for last update for schedule %s (%d)", //$NON-NLS-1$
								schedule.getName(),
								Integer.valueOf(schedule.getId())), re);
			}// try
		}// for
	}// modifyScheduleLastUpdate(Schedule)

	protected FeedConnection connection;

	protected List<FeedLesson> lessonsFeed;

	protected Schedule newSchedule;

	protected ScheduleUpdater scheduleDiff;

	protected void connectToAdeApi(Schedule schedule) throws AAAException {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_1;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		// Check network availability
		this.checkNetworkAvailability();

		this.connection = ContainerData.getFeedConnexion();
		if (this.connection == null) {
			String errmsg = this.getResources().getString(
					R.string.ADE_connection_failed);
			throw new AAAException(errmsg);
		}// if
	}// connectToAdeApi(Schedule)

	private void checkNetworkAvailability() throws NoConnectionException {
		ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
		if ((networkInfo == null) || !networkInfo.isConnected()) {
			throw new NoConnectionException();
		}// if
	}// checkNetWorkAvailability()

	protected void setProject(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_2;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		String sessionId = this.connection.getId();
		// this.connection =
		ContainerData.getFeedSetProject(sessionId, schedule.getProject()
				.getAdeProjectId());
	}// setProject(Schedule)

	protected void getLessonsFeed(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_3;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		String sessionId = this.connection.getId();
		schedule.setStudentNumList();
		this.lessonsFeed = ContainerData.getFeedsLesson(sessionId,
				schedule.getAllResources(), schedule.getPeriod());
		// On a fini la récupération de données depuis ADE, on se déconnecte
		this.connection = ContainerData.getFeedDeconnexion(sessionId);
	}// getLessonsFeed()

	protected void analyzeLessonsFeed(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_4;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		this.newSchedule = new Schedule();
		this.newSchedule.setId(schedule.getId());
		FeedAnalyzer feedAnalyzer = new FeedAnalyzer(this.newSchedule,
				this.lessonsFeed);
		feedAnalyzer.start();
	}// analyzeLessonsFeed()

	protected void compareWithPreviousSchedule(Schedule schedule, Database db) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_5;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		schedule.buildComplete(db);
		this.scheduleDiff = new GuidScheduleUpdater();
		this.scheduleDiff.compare(schedule, this.newSchedule);
	}// compareWithPreviousSchedule()

	protected void updateScheduleInsideDatabase(Schedule schedule, Database db)
			throws AAAException {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_6;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING);

		this.scheduleDiff.updateSchedule(db);
	}// updateScheduleInsideDatabase()

	/**
	 * 
	 */
	private void updateDeprecatedScheduleStates() {
		// Retrieve all schedules and keep the deprecated ones
		Database db = new Database(this);
		List<Schedule> deprecated = new ArrayList<Schedule>();
		String deprecatedLimt = getDeprecatedLimit();
		for (Schedule schedule : db.getAllSchedules()) {
			//			Log.d(TAG, String.format("[updateDeprecatedScheduleStates] " + //$NON-NLS-1$
			//					"%s(%d) - lastUpdate: '%s' - limit: '%s' ? deprecated: %b", //$NON-NLS-1$
			// schedule.getName(),
			// Integer.valueOf(schedule.getId()),
			// schedule.getLastUpdate(),
			// deprecatedLimt,
			// Boolean.valueOf(schedule.getLastUpdate().compareTo(deprecatedLimt)
			// < 0))) ;
			if (!schedule.getState().equals(ScheduleState.OTHER)
					&& schedule.getLastUpdate().compareTo(deprecatedLimt) < 0) {
				deprecated.add(schedule);
			}// if
		}// for

		// Update the state to OTHER for all deprecated schedules
		for (Schedule schedule : deprecated) {
			db.updateScheduleState(schedule.getId(), ScheduleState.OTHER);
			try {
				this.apiEndpoint.updateScheduleState(schedule.getId(),
						ScheduleState.OTHER);
			} catch (RemoteException re) {
				Log.d(TAG, "Failed to notify of schedule state change.", //$NON-NLS-1$
						re);
			}// try
		}// for
	}// updateDeprecatedScheduleStates()

	private static String getDeprecatedLimit() {
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.DAY_OF_YEAR, -DEPRECATED_LIMIT_IN_DAYS);
		Date limit = calendar.getTime();
		return Database.dateFormatter.format(limit);
	}// getDeprecatedLimit()

	protected void replicatePreferences(Bundle preferences) {
		String preferenceKey;
		SharedPreferences.Editor editor = PreferenceManager
				.getDefaultSharedPreferences(this).edit();

		// Auto-update
		preferenceKey = ResourceFetcher.getString(this,
				R.string.Preferences_sync);
		if (preferences.containsKey(preferenceKey)) {
			editor.putBoolean(preferenceKey,
					preferences.getBoolean(preferenceKey));
		}// if
			// Number of hours between two updates
		preferenceKey = ResourceFetcher.getString(this,
				R.string.Preferences_sync_delay);
		if (preferences.containsKey(preferenceKey)) {
			editor.putString(preferenceKey,
					preferences.getString(preferenceKey));
		}// if
			// Theme
		preferenceKey = ResourceFetcher.getString(this,
				R.string.Preferences_theme);
		if (preferences.containsKey(preferenceKey)) {
			editor.putString(preferenceKey,
					preferences.getString(preferenceKey));
		}// if

		editor.commit();
	}// replicatePreferences(Bundle)

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
			String key) {
		//		Log.d(TAG, String.format("[onSharedPreferenceChanged] key: '%s'\n" + //$NON-NLS-1$
		//				"\tSync: '%s' =?= %b" + //$NON-NLS-1$
		//				"\tnHrs: '%s' =?= %b", //$NON-NLS-1$
		// key,
		// ResourceFetcher.getString(this, R.string.Preferences_sync),
		// Boolean.valueOf(key.equals(ResourceFetcher.getString(this,
		// R.string.Preferences_sync))),
		// ResourceFetcher.getString(this, R.string.Preferences_sync_delay),
		// Boolean.valueOf(key.equals(ResourceFetcher.getString(this,
		// R.string.Preferences_sync_delay))))) ;

		if (key.equals(ResourceFetcher.getString(this,
				R.string.Preferences_sync))) {
			if (AdeWebOptions.autoUpdateIsOn(this)) {
				this.startAutoUpdates();
			} else {
				this.cancelAutoUpdates();
			}// if
		} else if (key.equals(ResourceFetcher.getString(this,
				R.string.Preferences_sync_delay))) {
			this.cancelAutoUpdates();
			this.startAutoUpdates();
		} else if (key.equals(ResourceFetcher.getString(this,
				R.string.Preferences_theme))) {
			// Log.d(TAG, "New theme, alert listeners") ;
			synchronized (this.listeners) {
				for (AdeWebServiceListener listener : this.listeners) {
					try {
						listener.handleModifiedOptions();
					} catch (RemoteException re) {
						Log.d(TAG, "Failed to notify of options change", re); //$NON-NLS-1$
					}// try
				}// for
			}// synchronized
		}// if
	}// onSharedPreferenceChanged(SharedPreferences, String)

	protected void launchAutoUpdate() {
		synchronized (this.updateQueue) {
			try {
				for (Schedule schedule : this.apiEndpoint.getAllSchedules()) {
					this.apiEndpoint.updateSchedule(schedule,
							schedule.getState());
				}// for
			} catch (RemoteException re) {
				Log.d(AdeWebService.TAG,
						"Failed to auto-update all schedules.", re); //$NON-NLS-1$
			}// try
		}// synchronized
	}// launchAutoUpdate()

}// class AdeWebService